/* Sentinel - IRC Statistical and Operator Services
** sock.c - TCP/IP socket management
**
** Copyright W. Campbell and others.  See README for more details
** Some code Copyright: Jonathan George, Kai Seidler, ircd-hybrid Team,
**                      IRCnet IRCD developers.
**
** $Id: sock.c,v 1.127 2004/04/02 02:26:02 wcampbel Exp $
*/

#include "stats.h"
#include "struct.h"
#include "proto.h"
#include "event.h"
#include "slog.h"
#include "mem.h"
#include <fcntl.h>

fd_set readfds, writefds, nullfds;
struct Connection *sconn = NULL;
int terminate_read_loop = 0;

/* Yes this is ugly */
#define DC_GOOD		0
#define DC_SERV		1
#define DC_DCC		2
static int deadconnflag = DC_GOOD;

static char sock_getbyte(struct Connection *);
static int sock_process_line(struct Connection *);

static void dcc_accept(struct Connection *);
static void dcc_estab(struct Connection *);
static void server_estab(struct Connection *);

#ifdef HAVE_GETADDRINFO
static struct addrinfo * gethostinfo(char const *host, int port);

int ConnectTo(char *host, int port)
{
  struct addrinfo *hostres, *res;
  struct addrinfo *bindres = NULL;
  int s = -1;
  int optval;
  int flags;

  hostres = gethostinfo(host, port);
  if (hostres == NULL)
  {
    slog(DEFAULT_LOG, L_ALERT, "FATAL:  Lookup error in CONNECT_TO, exiting");
    close_logfiles();
    exit(6);
  }
  if (IsVHost())
  {
    bindres = gethostinfo(settings.vhost, 0);
    if (bindres == NULL)
    {
      slog(DEFAULT_LOG, L_ALERT, "FATAL:  Lookup error in VHOST, exiting");
      close_logfiles();
      exit(6);
    }
  }
  for (res = hostres; res; res = res->ai_next)
  {
    s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (s < 0)
      continue;

    if (s > me.max_fd)
      me.max_fd = s;

    optval = 1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&optval,
               sizeof(optval));

    if (IsVHost())
    {
      if (bind(s, bindres->ai_addr, bindres->ai_addrlen) < 0)
      {
        slog(DEFAULT_LOG, L_CRIT, "Cannot bind to VHost");
        return ( -1);
      }
    }

    flags = fcntl(s, F_GETFL, 0);
    flags |= O_NONBLOCK;
    (void) fcntl(s, F_SETFL, flags);

    connect(s, res->ai_addr, res->ai_addrlen);
    break;
    /* XXX What to do about any hosts after this?
    ** it basically just connects to the first one listed
    ** right now.
    */
  }
  freeaddrinfo(hostres);
  if (IsVHost())
  {
    freeaddrinfo(bindres);
  }
  if (res == NULL)
  {
    slog(DEFAULT_LOG, L_ERR, "Cannot connect to host");
    return ( -1);
  }
  sconn = add_connection(s, "Server Uplink", CONN_SERV | CONN_CONNECTING);

  return s;
}
#else 
/* Initiate the connection to the server, returns the FD */
int ConnectTo(char *host, int port)
{
  struct hostent *hp;
  struct sockaddr_in sa;
  struct in_addr * in;
  int s;
  int optval;
  int flags;

  if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  {
    slog(DEFAULT_LOG, L_CRIT, "Cannot create socket()");
    return ( -1);
  }

  if (s > me.max_fd)
    me.max_fd = s;

  if (IsVHost())
  {
    memset((void *) &sa, '\0', sizeof(sa));
    sa.sin_family = AF_INET;
    if ((hp = gethostbyname (settings.vhost)) == NULL)
    {
      close(s);
      slog(DEFAULT_LOG, L_CRIT, "Unable to gethostbyname(%s)",
           settings.vhost);
      return ( -1);
    }
    in = (struct in_addr *) (hp->h_addr_list[0]);
    sa.sin_addr.s_addr = in->s_addr;
    sa.sin_port = 0;

    optval = 1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&optval,
               sizeof(optval));

    if (bind(s, (struct sockaddr *) &sa, sizeof(sa)) < 0)
    {
      close(s);
      slog(DEFAULT_LOG, L_CRIT, "Unable to bind to VHOST %s",
           settings.vhost);
      return ( -1);
    }
  }

  if ((hp = gethostbyname (host)) == NULL)
  {
    close(s);
    slog(DEFAULT_LOG, L_CRIT, "Cannot gethostbyname(%s)", host);
    return ( -1);
  }

  memset((void *) &sa, '\0', sizeof(sa));
  sa.sin_family = AF_INET;
  sa.sin_port = htons (port);
  memcpy((void *) &sa.sin_addr, (void *) hp->h_addr, hp->h_length);

  flags = fcntl(s, F_GETFL, 0);
  flags |= O_NONBLOCK;
  (void) fcntl(s, F_SETFL, flags);

  connect (s, (struct sockaddr *) &sa, sizeof (sa));

  sconn = add_connection(s, "Server Uplink", CONN_SERV | CONN_CONNECTING);
  return s;
}
#endif

/* Here is where it all happens, get a line from the server
** and send it off to the parse function.  Also this is where
** the call to the event loop is issued.
*/
void read_loop()
{
  int SelectResult;
  struct timeval TimeOut;
  dlink_node *dl;
  dlink_node *tdl;
  struct Connection *conn;

  terminate_read_loop = 0;

  while (TRUE)
  {
    /* Must be updated prior to eventRun() */
    CurrentTime = time(NULL);
    eventRun();
    if (terminate_read_loop)
      return;
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);
    TimeOut.tv_sec = 1;
    TimeOut.tv_usec = 0L;

    /* Any Connection struct left over in the dying list is now safe to
    ** destroy.
    */
    DLINK_FOREACH_SAFE(dl, tdl, dying_list.head)
    {
      conn = (struct Connection *) dl->data;

      dlink_delete(dl, &dying_list);
      free(conn);
      memcounts.connection--;
      dlink_free(dl);
    }

    DLINK_FOREACH(dl, connection_list.head)
    {
      conn = (struct Connection *) dl->data;
      if (conn->flags & CONN_CONNECTING)
      {
        if (conn->flags & CONN_DCCOUT)
          FD_SET(conn->fd, &writefds);
        else if (conn->flags & CONN_DCCIN)
          FD_SET(conn->fd, &readfds);
        else if (conn->flags & CONN_SERV)
          FD_SET(conn->fd, &writefds);
      }
      else
        FD_SET(conn->fd, &readfds);
    }

    /* me.max_fd used instead of FD_SETSIZE, in order to save some CPU.
    ** This will gradually increase over time, and never decrease.
    */
    if ((SelectResult = select(me.max_fd + 1, &readfds, &writefds,
                               &nullfds, &TimeOut)) > 0)
    {
      /* Must be updated after running select */
      CurrentTime = time(NULL);

      DLINK_FOREACH_SAFE(dl, tdl, connection_list.head)
      {
        conn = (struct Connection *) dl->data;
        if (FD_ISSET(conn->fd, &readfds))
        {
          int retval;

          if (conn->flags & CONN_CONNECTING)
          {
            if (conn->flags & CONN_DCCIN)
              dcc_accept(conn);
            /* else ??? */
            continue;
          }
          /* It's a connected socket */
          retval = sock_process_line(conn);
          if (retval == DC_SERV)
          {
            deadconnflag = DC_GOOD;
            return ;
          }
          /* deadconnflag DC_DCC is no error, reset */
          deadconnflag = DC_GOOD;
          continue;
        }
        if (FD_ISSET(conn->fd, &writefds))
        {
          if (conn->flags & CONN_CONNECTING)
          {
            if (conn->flags & CONN_DCCOUT)
            {
              dcc_estab(conn);
              continue;
            }
            if (conn->flags & CONN_SERV)
            {
              server_estab(conn);
              continue;
            }
          }
        }
      }
    }
    else
    {
      /* select returns 0 when the TimeOut is hit */
      if (SelectResult == 0)
      {
        /* simply a timeout, needed for eventRun() */
      }
      else if ((SelectResult == -1) && (errno == EINTR))
      {
        /* Interrupted system call, we got a signal, simply restart the
        ** select loop. */
      }
      else
      {
        /* select returned -1, an error condition of
        ** some kind (we don't care what) occurred.
        */
        slog(DEFAULT_LOG, L_CRIT, "Lost connection to server: %d: %s",
             errno, strerror(errno));
        close(servsock);
        ServTermination();
        delete_connection(servsock);
        servsock = -1;
        return ;
      }
    }
  }
}

static void dcc_accept(struct Connection *conn)
{
  int accept_sock;
  struct sockaddr_in *incoming_addr;
  int addrlen = sizeof(struct sockaddr);

  /* clear errno before this begins */
  errno = 0;

  incoming_addr = smalloc(sizeof(struct sockaddr_in));

  if ((accept_sock = accept(conn->fd, (struct sockaddr *)incoming_addr,
                            (socklen_t *)&addrlen)) < 0)
  {
    if (errno == EAGAIN)
      return ;
    notice(conn->name, AdminServ.nick, "DCC CHAT Failed");
    shutdown(conn->fd, SHUT_RDWR);
    close(conn->fd);
    conn->flags |= CONN_DEAD;
    delete_connection(conn->fd);
    free(incoming_addr);
    return ;
  }

  if (accept_sock > me.max_fd)
    me.max_fd = accept_sock;

  /* close the listening socket */
  shutdown(conn->fd, SHUT_RDWR);
  close(conn->fd);

  conn->fd = accept_sock;
  conn->sa = incoming_addr;
  conn->flags &= ~CONN_CONNECTING;
  /* Send out the welcome message */
  sendto_dcc(conn->fd, "Connected to %s: %s", AdminServ.nick,
             settings.name);
  sendto_all_dcc("Client %s connected", conn->name);
  slog(DEFAULT_LOG, L_NOTICE, "Client %s connected (FD:%d)[%s]", conn->name, conn->fd,
       inet_ntoa(conn->sa->sin_addr));
  if (IsUnauthChat())
    conn->flags |= CF_CHAT;
  conn->last = CurrentTime;
}

static void dcc_estab(struct Connection *conn)
{
  int ret;

  /* Send a welcome message, this will catch if the connection
  ** fails by erroring it out
  */
  ret = sendto_dcc(conn->fd, "Connected to %s: %s", AdminServ.nick,
                   settings.name);
  if (ret == 0)
  {
    struct sockaddr_in *sa;
    socklen_t salen = sizeof(struct sockaddr);

    /* This socket has connected */
    conn->flags &= ~CONN_CONNECTING;
    /* As long as it returns "valid" */
    sendto_all_dcc("Client %s connected", conn->name);
    if (IsUnauthChat())
      conn->flags |= CF_CHAT;
    sa = smalloc(sizeof(struct sockaddr_in));
    getpeername(conn->fd, (struct sockaddr *)sa, &salen);
    conn->sa = sa;
    slog(DEFAULT_LOG, L_NOTICE, "Client %s connected (FD:%d)[%s]", conn->name,
         conn->fd, inet_ntoa(conn->sa->sin_addr));
    conn->last = CurrentTime;
  }
}

static void server_estab(struct Connection *conn)
{
  int ret;

  servsock = conn->fd;

  ret = login();
  if (ret == 1)
  {
    /* XXX connections to multiple servers */
    slog(DEFAULT_LOG, L_ERR, "Unable to connect to %s port %d", settings.uplink,
         settings.port);
    servsock = -1;
    delete_connection(conn->fd);
    return ;
  }

  /* Wait until we are sure we are connected before starting up
  ** the events
  */
  StartInitialEvents();
  conn->flags &= ~CONN_CONNECTING;
}

static char sock_getbyte(struct Connection *conn)
{
  static char message[BUFSIZE + 1];
  static char *c = message;
  char rc;
  int n;
  static int data_in_buffer = 0;

  if (data_in_buffer == 0)
  {
    n = read(conn->fd, message, BUFSIZE);
    if (n <= 0)
    {
      if (conn->fd == servsock)
      {
        slog(DEFAULT_LOG, L_CRIT, "Lost connection to server.");
        shutdown(servsock, SHUT_RDWR);
        close(servsock);
        delete_connection(conn->fd);
        ServTermination();
        servsock = -1;
        deadconnflag = DC_SERV;
        return '\0';
      }
      else
      {
        /* One of the DCC sockets died */
        slog(DEFAULT_LOG, L_NOTICE, "Lost connection to dcc socket %d",
             conn->fd);
        shutdown(conn->fd, SHUT_RDWR);
        close(conn->fd);
        conn->flags |= CONN_DEAD;
        sendto_all_dcc("Client %s disconnected",
                       conn->name);
        slog(DEFAULT_LOG, L_NOTICE, "Client %s disconnected "
             "(FD:%d)[%s]", conn->name, conn->fd,
             inet_ntoa(conn->sa->sin_addr));
        delete_connection(conn->fd);
        deadconnflag = DC_DCC;
        return '\0';
      }
    }
    message[n] = '\0';
    c = message;
    data_in_buffer = 1;
  }
  if (*c == '\0')
  {
    data_in_buffer = 0;
    message[0] = '\0';
    return *c;
  }
  rc = *c;
  c++;
  return rc;
}

static int sock_process_line(struct Connection *conn)
{
  char message[BUFSIZE + 1];
  int i = 0;
  char c;

  if (conn->hbuffer[0] != '\0')
  {
    strlcpy(message, conn->hbuffer, (BUFSIZE + 1));
    i = strlen(message);
    message[i] = '\0';
    conn->hbuffer[0] = '\0';
  }

  while (TRUE)
  {
    c = sock_getbyte(conn);
    if (deadconnflag >= 1)
      return deadconnflag;
    if (c == '\0')
    {
      if (i > 0)
      {
        message[i] = '\0';
        strlcpy(conn->hbuffer, message, BUFSIZE + 1);
      }
      break;
    }
    if ((c == '\r') || (c == '\n'))
    {
      if (i > 0)
      {
        message[i] = '\0';
        netinfo.inb += i;
        if (conn->fd == servsock)
          parse(message);
        else
          dcc_parse(conn, message);
        i = 0;
      }
      continue;
    }
    /* Anything else */
    message[i] = c;
    i++;
    /* Because sock_getbyte() will only read in BUFSIZE bytes,
    ** we'll never overflow here.
    */
  }
  return 0;
}

#ifdef HAVE_GETADDRINFO 
/* Stolen from FreeBSD's whois client, modified for Sentinel by W. Campbell */
static struct addrinfo * gethostinfo(char const *host, int port)
{
  struct addrinfo hints, *res;
  int error;
  char portbuf[6];

  memset(&hints, 0, sizeof(hints));
  hints.ai_flags = 0;
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  snprintf(portbuf, 6, "%d", port);
  error = getaddrinfo(host, portbuf, &hints, &res);
  if (error)
  {
    slog(DEFAULT_LOG, L_CRIT, "gethostinfo error: %s: %s", host,
         gai_strerror(error));
    return (NULL);
  }
  return (res);
}
#endif

void close_dccs(void)
{
  dlink_node *dl;
  dlink_node *dtmp;
  struct Connection *conn;

  DLINK_FOREACH_SAFE(dl, dtmp, connection_list.head)
  {
    conn = (struct Connection *) dl->data;
    if (conn->flags & (CONN_DCCIN | CONN_DCCOUT))
    {
      shutdown(conn->fd, SHUT_RDWR);
      close(conn->fd);
      delete_connection(conn->fd);
    }
  }
}

struct hostent *get_hostname(char *para)
{
  char hostname[MAXHOST];
  char *host;
  struct hostent *local_host = NULL;
  struct hostent *host_ptr;

  if (para == NULL)
  {
    gethostname(hostname, MAXHOST - 1);
    hostname[MAXHOST - 1] = '\0';
    host = hostname;
  }
  else
  {
    host = para;
  }
  host_ptr = gethostbyname(host);
  if (host_ptr != NULL)
  {
    local_host = smalloc(sizeof(struct hostent));
    memcpy(local_host, host_ptr, sizeof(struct hostent));
  }

  return local_host;
}
